From: Colin Walters Date: Mon, 30 Jun 2025 21:07:18 +0000 (-0400) Subject: soft-reboot: Many changes X-Git-Tag: archive/raspbian/2025.7-2+rpi1^2^2~6^2~4^2~6^2 X-Git-Url: https://dgit.raspbian.org/%22http:/www.example.com/cgi/%22https://%22%22/%22http:/www.example.com/cgi/%22https:/%22%22?a=commitdiff_plain;h=6bee4a08e3294866256d08295a97652faa9e6181;p=ostree.git soft-reboot: Many changes - Add --reboot and --reset arguments - Don't compile on centos stream 9 (missing `open_tree` glibc wrapper) as the functionality isn't supported by systemd there; that said we should also do dynamic detection - Fix /sysroot writability - If we target as soft reboot a deployment *other* than the staged one, automatically clear the staged deployment as otherwise the semantics are too confusing. - Rename the APIs so they all say `soft_reboot` and not `next_root` Signed-off-by: Colin Walters --- diff --git a/apidoc/ostree-sections.txt b/apidoc/ostree-sections.txt index 2ad402cb..a43d1079 100644 --- a/apidoc/ostree-sections.txt +++ b/apidoc/ostree-sections.txt @@ -591,7 +591,8 @@ ostree_sysroot_deployment_set_mutable ostree_sysroot_deployment_unlock ostree_sysroot_deployment_set_pinned ostree_sysroot_deployment_can_soft_reboot -ostree_sysroot_deployment_prepare_next_root +ostree_sysroot_deployment_set_soft_reboot +ostree_sysroot_clear_soft_reboot ostree_sysroot_write_deployments ostree_sysroot_write_deployments_with_options ostree_sysroot_write_origin_file diff --git a/configure.ac b/configure.ac index 5e32d3dc..80237af0 100644 --- a/configure.ac +++ b/configure.ac @@ -97,6 +97,12 @@ AC_CHECK_FUNCS([nanotime clock_gettime]) AC_STRUCT_TIMEZONE AC_CHECK_HEADER([sys/xattr.h],,[AC_MSG_ERROR([You must have sys/xattr.h from glibc])]) +dnl new mount api +AC_CHECK_FUNCS([open_tree]) +AM_CONDITIONAL([HAVE_SOFT_REBOOT], [test x$ac_cv_func_open_tree = xyes]) +AM_COND_IF([HAVE_SOFT_REBOOT], + [AC_DEFINE([HAVE_SOFT_REBOOT], 1, [Define if we have soft reboots])]) + AS_IF([test "$YACC" != "bison -y"], [AC_MSG_ERROR([bison not found but required])]) AC_SUBST([LIBS_PRIVATE]) diff --git a/man/ostree-admin-prepare-soft-reboot.xml b/man/ostree-admin-prepare-soft-reboot.xml index fa34457b..f6b02211 100644 --- a/man/ostree-admin-prepare-soft-reboot.xml +++ b/man/ostree-admin-prepare-soft-reboot.xml @@ -33,13 +33,41 @@ SPDX-License-Identifier: LGPL-2.0+ Description - Prepare the deployment at INDEX for a systemd soft reboot. INDEX must be in range and not reference the currently booted deployment. - It is recommended to immediately follow this with an involcation of systemctl soft-reboot. + Prepare (or unset) the deployment at INDEX for a systemd soft reboot by mounting /run/nextroot. + INDEX must be in range and not reference the currently booted deployment. - It is not supported to soft reboot into a deployment with a different kernel than the booted one. + It is not supported to soft reboot into a deployment with a different kernel state (including initramfs) than the booted one. + + + If a soft reboot is initiated for a deployment that is not staged, while a staged deployment is active, the staged + deployment will be automatically cleared. + + + + + Options + + + + + + + Initiate a soft reboot. + + + + + + + + Clear a pending soft reboot state instead of initializing one. When this is provided, + a deployment index is not required. + + + diff --git a/src/libostree/libostree-devel.sym b/src/libostree/libostree-devel.sym index 4a46a0b3..610a36b7 100644 --- a/src/libostree/libostree-devel.sym +++ b/src/libostree/libostree-devel.sym @@ -34,5 +34,6 @@ LIBOSTREE_2025.3 { global: ostree_deployment_is_soft_reboot_target; ostree_sysroot_deployment_can_soft_reboot; - ostree_sysroot_deployment_prepare_next_root; + ostree_sysroot_deployment_set_soft_reboot; + ostree_sysroot_clear_soft_reboot; } LIBOSTREE_2025.2; diff --git a/src/libostree/ostree-soft-reboot.c b/src/libostree/ostree-soft-reboot.c index 08fc909b..e44db255 100644 --- a/src/libostree/ostree-soft-reboot.c +++ b/src/libostree/ostree-soft-reboot.c @@ -42,6 +42,7 @@ gboolean _ostree_prepare_soft_reboot (GError **error) { +#ifdef HAVE_SOFT_REBOOT const char *sysroot_path = "/sysroot"; const char *target_deployment = "."; @@ -81,10 +82,24 @@ _ostree_prepare_soft_reboot (GError **error) if (!otcore_mount_etc (config, &metadata_builder, OTCORE_RUN_NEXTROOT, error)) return FALSE; - // Note we should have inherited the readonly sysroot - g_autofree char *target_sysroot = g_build_filename (OTCORE_RUN_NEXTROOT, "sysroot", NULL); - if (mount (sysroot_path, target_sysroot, NULL, MS_BIND | MS_SILENT, NULL) < 0) - return glnx_throw_errno_prefix (error, "failed to bind mount sysroot"); + // And set up /sysroot. Here since we hardcode composefs, we also hardcode + // having a read-only /sysroot. + g_variant_builder_add (&metadata_builder, "{sv}", OTCORE_RUN_BOOTED_KEY_SYSROOT_RO, + g_variant_new_boolean (true)); + { + struct mount_attr attr = { .attr_set = MOUNT_ATTR_RDONLY }; + glnx_autofd int sysroot_fd + = open_tree (AT_FDCWD, sysroot_path, OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + if (sysroot_fd < 0) + return glnx_throw_errno_prefix (error, "open_tree(%s)", sysroot_path); + if (mount_setattr (sysroot_fd, "", AT_EMPTY_PATH, &attr, sizeof (struct mount_attr)) < 0) + return glnx_throw_errno_prefix (error, "syscall(mount_setattr) of sysroot"); + g_autofree char *target_sysroot = g_build_filename (OTCORE_RUN_NEXTROOT, "sysroot", NULL); + if (move_mount (sysroot_fd, "", -1, target_sysroot, MOVE_MOUNT_F_EMPTY_PATH) < 0) + return glnx_throw_errno_prefix (error, "syscall(move_mount) of sysroot"); + + g_debug ("initialized /sysroot"); + } /* This can be used by other things to signal ostree is in use */ { @@ -96,4 +111,7 @@ _ostree_prepare_soft_reboot (GError **error) } return TRUE; +#else + return glnx_throw (error, "soft reboot not supported"); +#endif } diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index 9c7a1987..98d296a2 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -66,6 +66,8 @@ */ #define EARLY_PRUNE_SAFETY_MARGIN_SIZE (1 << 20) /* 1 MB */ +static void impl_clear_soft_reboot (void); + /* * Like symlinkat() but overwrites (atomically) an existing * symlink. @@ -2881,6 +2883,22 @@ ostree_sysroot_write_deployments_with_options (OstreeSysroot *self, GPtrArray *n } const guint nonstaged_current_len = self->deployments->len - (self->staged_deployment ? 1 : 0); + gboolean removed_soft_reboot_target = (self->soft_reboot_target_deployment != NULL); + for (guint i = 0; i < new_deployments->len; i++) + { + OstreeDeployment *deployment = new_deployments->pdata[i]; + if (ostree_deployment_is_soft_reboot_target (deployment)) + { + removed_soft_reboot_target = FALSE; + break; + } + } + if (removed_soft_reboot_target) + { + g_debug ("Removing soft reboot target"); + impl_clear_soft_reboot (); + } + /* Assign a bootserial to each new deployment. */ assign_bootserials (new_deployments); @@ -3756,6 +3774,25 @@ ostree_sysroot_stage_tree (OstreeSysroot *self, const char *osname, const char * &opts, out_new_deployment, cancellable, error); } +/* Ensure ostree-finalize-staged.service is started */ +gboolean +_ostree_sysroot_ensure_finalize_staged_service (GError **error) +{ + // The service which performs finalization + const char *svc = "ostree-finalize-staged.service"; + + const char *const systemctl_argv[] = { "systemctl", "start", "--quiet", svc, NULL }; + int estatus; + if (!g_spawn_sync (NULL, (char **)systemctl_argv, NULL, + G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, NULL, + &estatus, error)) + return FALSE; + if (!g_spawn_check_exit_status (estatus, error)) + return glnx_prefix_error (error, "Failed to start %s", svc); + + return TRUE; +} + /** * ostree_sysroot_stage_tree_with_options: * @self: Sysroot @@ -3782,8 +3819,6 @@ ostree_sysroot_stage_tree_with_options (OstreeSysroot *self, const char *osname, GCancellable *cancellable, GError **error) { GLNX_AUTO_PREFIX_ERROR ("Staging deployment", error); - // The service which performs finalization - const char *svc = "ostree-finalize-staged.service"; if (!_ostree_sysroot_ensure_writable (self, error)) return FALSE; @@ -3792,14 +3827,12 @@ ostree_sysroot_stage_tree_with_options (OstreeSysroot *self, const char *osname, if (booted_deployment == NULL) return glnx_prefix_error (error, "Cannot stage deployment"); - const char *const systemctl_argv[] = { "systemctl", "start", "--quiet", svc, NULL }; - int estatus; - if (!g_spawn_sync (NULL, (char **)systemctl_argv, NULL, - G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, NULL, - &estatus, error)) + // Staging always resets soft reboot state by default + if (!ostree_sysroot_clear_soft_reboot (self, cancellable, error)) + return FALSE; + + if (!_ostree_sysroot_ensure_finalize_staged_service (error)) return FALSE; - if (!g_spawn_check_exit_status (estatus, error)) - return glnx_prefix_error (error, "Failed to start %s", svc); g_autoptr (OstreeDeployment) deployment = NULL; if (!sysroot_initialize_deployment (self, osname, revision, origin, opts, &deployment, @@ -3953,21 +3986,55 @@ ostree_sysroot_change_finalization (OstreeSysroot *self, OstreeDeployment *deplo return TRUE; } +struct PrepareRootChildSetupContext +{ + const char *deployment_path; + int rootns_fd; +}; + +static inline void +prepare_root_child_setup (gpointer data) +{ + struct PrepareRootChildSetupContext *ctx = data; + // Enter the root namespace first to escape the overlayfs context + int rc = setns (ctx->rootns_fd, CLONE_NEWNS); + if (rc < 0) + err (1, "setns"); + // Then change to the deployment directory in the root namespace + rc = chdir (ctx->deployment_path); + if (rc < 0) + err (1, "chdir"); +} + +static gboolean _ostree_sysroot_finalize_impl_staged_deployment (OstreeSysroot *self, + GCancellable *cancellable, + GError **error); + /* Invoked at shutdown time by ostree-finalize-staged.service */ static gboolean _ostree_sysroot_finalize_staged_inner (OstreeSysroot *self, GCancellable *cancellable, GError **error) { - /* It's totally fine if there's no staged deployment; perhaps down the line - * though we could teach the ostree cmdline to tell systemd to activate the - * service when a staged deployment is created. - */ - if (!self->staged_deployment) + /* Check if we have anythign to do */ + if (!self->staged_deployment && !self->soft_reboot_target_deployment) { - ot_journal_print (LOG_INFO, "No deployment staged for finalization"); + ot_journal_print (LOG_INFO, "No deployment staged for finalization or soft reboot"); return TRUE; } + if (!_ostree_sysroot_finalize_impl_staged_deployment (self, cancellable, error)) + return FALSE; + + return TRUE; +} + +static gboolean +_ostree_sysroot_finalize_impl_staged_deployment (OstreeSysroot *self, GCancellable *cancellable, + GError **error) +{ + if (!self->staged_deployment) + return TRUE; + /* Check if finalization is locked. */ gboolean locked = false; (void)g_variant_lookup (self->staged_deployment_data, _OSTREE_SYSROOT_STAGED_KEY_LOCKED, "b", @@ -4285,26 +4352,6 @@ ostree_sysroot_deployment_set_mutable (OstreeSysroot *self, OstreeDeployment *de return TRUE; } -struct PrepareRootChildSetupContext -{ - const char *deployment_path; - int rootns_fd; -}; - -static inline void -prepare_root_child_setup (gpointer data) -{ - struct PrepareRootChildSetupContext *ctx = data; - // Enter the root namespace first to escape the overlayfs context - int rc = setns (ctx->rootns_fd, CLONE_NEWNS); - if (rc < 0) - err (1, "setns"); - // Then change to the deployment directory in the root namespace - rc = chdir (ctx->deployment_path); - if (rc < 0) - err (1, "chdir"); -} - /** * ostree_sysroot_deployment_can_soft_reboot: * @self: The #OstreeSysroot object. @@ -4331,8 +4378,21 @@ ostree_sysroot_deployment_can_soft_reboot (OstreeSysroot *self, OstreeDeployment return false; } +static void +impl_clear_soft_reboot (void) +{ + int flags = G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL; + // If we failed to initialize the soft reboot, ensure that we've unwound any mounts + const char *umount_argv[] = { "umount", "-R", "/run/nextroot", NULL }; + // To aid debugging allow skipping cleanup on failure + if (!g_getenv ("OSTREE_SKIP_NEXTROOT_CLEANUP")) + g_spawn_sync (NULL, (char **)umount_argv, NULL, flags, NULL, NULL, NULL, NULL, NULL, NULL); + + (void)unlinkat (AT_FDCWD, OTCORE_RUN_NEXTROOT_BOOTED, 0); +} + /** - * ostree_sysroot_deployment_prepare_next_root + * ostree_sysroot_deployment_set_soft_reboot: * @self: Sysroot * @deployment: Deployment to prepare /run/nextroot * @allow_kernel_skew: Continue even if there is a kernel mismatch @@ -4345,30 +4405,40 @@ ostree_sysroot_deployment_can_soft_reboot (OstreeSysroot *self, OstreeDeployment * Since: TODO */ gboolean -ostree_sysroot_deployment_prepare_next_root (OstreeSysroot *self, OstreeDeployment *deployment, - gboolean allow_kernel_skew, GCancellable *cancellable, - GError **error) +ostree_sysroot_deployment_set_soft_reboot (OstreeSysroot *self, OstreeDeployment *deployment, + gboolean allow_kernel_skew, GCancellable *cancellable, + GError **error) { +#ifdef HAVE_SOFT_REBOOT GLNX_AUTO_PREFIX_ERROR ("Preparing /run/nextroot for a soft-reboot", error); if (!ostree_sysroot_deployment_can_soft_reboot (self, deployment) && !allow_kernel_skew) - { - return glnx_throw (error, "Cannot soft-reboot to deployment with different kernel"); - } + return glnx_throw (error, "Cannot soft-reboot to deployment with different kernel state"); - // For targeting a staged deployment, we finalize now to ensure that we have /etc - if (ostree_deployment_is_staged (deployment)) + if (!_ostree_sysroot_ensure_finalize_staged_service (error)) + return FALSE; + + // Preparing a soft reboot while a staged deployment is active, but targeting + // a deployment other than the staged one will unset the staged state. + if (self->staged_deployment != NULL && deployment != self->staged_deployment) { - if (!_ostree_sysroot_finalize_staged (self, NULL, error)) + g_autoptr (GPtrArray) current_deployments = ostree_sysroot_get_deployments (self); + g_assert (current_deployments->len > 0); + g_assert (current_deployments->pdata[0] == self->staged_deployment); + g_ptr_array_remove_index (current_deployments, 0); + if (!ostree_sysroot_write_deployments (self, current_deployments, cancellable, error)) return FALSE; } g_autofree char *deployment_relpath = ostree_sysroot_get_deployment_dirpath (self, deployment); + // We only support queuing a soft reboot from a booted host right now, so ignore self->sysroot_fd + g_assert (self->booted_deployment); g_autofree char *deployment_fullpath = g_build_filename ("/sysroot", deployment_relpath, NULL); gint estatus; const char *argv[] = { "ostree", "admin", "impl-prepare-soft-reboot", NULL }; + // The outer CLI entered a mount namespace; escape it glnx_autofd int rootns_fd = -1; if (!glnx_openat_rdonly (AT_FDCWD, "/proc/1/ns/mnt", TRUE, &rootns_fd, error)) return FALSE; @@ -4384,18 +4454,41 @@ ostree_sysroot_deployment_prepare_next_root (OstreeSysroot *self, OstreeDeployme if (!g_spawn_check_exit_status (estatus, error)) { - int flags = G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL; - // If we failed to initialize the soft reboot, ensure that we've unwound any mounts - const char *umount_argv[] = { "umount", "-R", "/run/nextroot", NULL }; - // To aid debugging allow skipping cleanup on failure - if (!g_getenv ("OSTREE_SKIP_NEXTROOT_CLEANUP")) - g_spawn_sync (NULL, (char **)umount_argv, NULL, flags, NULL, NULL, NULL, NULL, NULL, NULL); + impl_clear_soft_reboot (); return FALSE; } - ot_journal_print (LOG_INFO, "Set up soft reboot at /run/nextroot"); + g_debug ("Soft reboot setup complete"); - return TRUE; + // Last step + return write_deployments_finish (self, cancellable, error); +#else + return glnx_throw (error, "soft reboot not supported"); +#endif +} + +/** + * ostree_sysroot_clear_soft_reboot: + * @self: Sysroot + * @cancellable: Cancellable + * @error: Error + * + * If there is a soft reboot queued in /run/nextroot, clear it. If one + * is not queued, this function successfully does nothing. + * + * Since: TODO + */ +gboolean +ostree_sysroot_clear_soft_reboot (OstreeSysroot *self, GCancellable *cancellable, GError **error) +{ + if (!self->soft_reboot_target_deployment) + return TRUE; + + impl_clear_soft_reboot (); + + g_debug ("Cleared soft reboot queued state"); + + return write_deployments_finish (self, cancellable, error); } /** diff --git a/src/libostree/ostree-sysroot-private.h b/src/libostree/ostree-sysroot-private.h index 1ca12c26..f5c70872 100644 --- a/src/libostree/ostree-sysroot-private.h +++ b/src/libostree/ostree-sysroot-private.h @@ -83,7 +83,7 @@ struct OstreeSysroot dev_t root_device; ino_t root_inode; /* The device inode for a queued soft reboot deployment */ - gboolean have_nextroot; + gboolean expecting_nextroot; dev_t nextroot_device; ino_t nextroot_inode; @@ -95,6 +95,7 @@ struct OstreeSysroot int bootversion; int subbootversion; OstreeDeployment *booted_deployment; + OstreeDeployment *soft_reboot_target_deployment; OstreeDeployment *staged_deployment; GVariant *staged_deployment_data; // True if loaded_ts is initialized @@ -157,6 +158,8 @@ void _ostree_deployment_set_bootconfig_from_kargs (OstreeDeployment *deployment, gboolean _ostree_sysroot_reload_staged (OstreeSysroot *self, GError **error); +gboolean _ostree_sysroot_ensure_finalize_staged_service (GError **error); + gboolean _ostree_sysroot_finalize_staged (OstreeSysroot *self, GCancellable *cancellable, GError **error); gboolean _ostree_sysroot_boot_complete (OstreeSysroot *self, GCancellable *cancellable, diff --git a/src/libostree/ostree-sysroot.c b/src/libostree/ostree-sysroot.c index 3e936174..40498411 100644 --- a/src/libostree/ostree-sysroot.c +++ b/src/libostree/ostree-sysroot.c @@ -961,7 +961,9 @@ parse_deployment (OstreeSysroot *self, const char *boot_link, OstreeDeployment * ret_deployment->device = stbuf.st_dev; ret_deployment->inode = stbuf.st_ino; - g_debug ("Deployment %s.%d unlocked=%d", treecsum, deployserial, ret_deployment->unlocked); + g_debug ("Deployment %s.%d unlocked=%d dev=%" G_GUINT64_FORMAT " ino=%" G_GUINT64_FORMAT, + treecsum, deployserial, ret_deployment->unlocked, ret_deployment->device, + ret_deployment->inode); if (is_booted_deployment) self->booted_deployment = g_object_ref (ret_deployment); @@ -1224,14 +1226,18 @@ _ostree_sysroot_reload_soft_reboot (OstreeSysroot *self, GError **error) { GLNX_AUTO_PREFIX_ERROR ("Loading nextroot", error); // Reset state - self->have_nextroot = FALSE; + self->expecting_nextroot = FALSE; + g_clear_object (&self->soft_reboot_target_deployment); glnx_autofd int fd = -1; if (!ot_openat_ignore_enoent (AT_FDCWD, OTCORE_RUN_NEXTROOT_BOOTED, &fd, error)) return FALSE; // If there's no such file, we're done if (fd == -1) - return TRUE; + { + g_debug ("No %s", OTCORE_RUN_NEXTROOT_BOOTED); + return TRUE; + } // Parse the GVariant metadata from this; search for OTCORE_RUN_BOOTED_KEY_BACKING_ROOTDEVINO // to find similar code. @@ -1246,10 +1252,12 @@ _ostree_sysroot_reload_soft_reboot (OstreeSysroot *self, GError **error) if (!backing_devino) return glnx_throw (error, "Missing %s key in %s", OTCORE_RUN_BOOTED_KEY_BACKING_ROOTDEVINO, OTCORE_RUN_NEXTROOT_BOOTED); - // Load the device/inode, and we're done g_variant_get (backing_devino, "(tt)", &backing_dev, &backing_ino); - self->have_nextroot = TRUE; + g_debug ("Expecting nextroot dev %" G_GUINT64_FORMAT " ino %" G_GUINT64_FORMAT, backing_dev, + backing_ino); + + self->expecting_nextroot = TRUE; self->nextroot_device = (dev_t)backing_dev; self->nextroot_inode = (ino_t)backing_ino; @@ -1327,19 +1335,30 @@ sysroot_load_from_bootloader_configs (OstreeSysroot *self, GCancellable *cancell g_ptr_array_insert (deployments, 0, g_object_ref (self->staged_deployment)); /* Synchronize internal state now that we've loaded all deployments */ + g_debug ("expecting nextroot: %d", self->expecting_nextroot); for (guint i = 0; i < deployments->len; i++) { OstreeDeployment *deployment = deployments->pdata[i]; ostree_deployment_set_index (deployment, i); g_assert (deployment->devino_initialized); - if (self->have_nextroot && deployment->device == self->nextroot_device + if (self->expecting_nextroot && deployment->device == self->nextroot_device && deployment->inode == self->nextroot_inode) { deployment->soft_reboot_target = TRUE; + g_assert (!self->soft_reboot_target_deployment); + self->soft_reboot_target_deployment = g_object_ref (deployment); } } + if (self->expecting_nextroot && !self->soft_reboot_target_deployment) + { + g_debug ("Soft reboot target not found"); + if (!glnx_unlinkat (AT_FDCWD, OTCORE_RUN_NEXTROOT_BOOTED, 0, error)) + return FALSE; + self->expecting_nextroot = FALSE; + } + /* Determine whether we're "physical" or not, the first time we load deployments */ if (self->loadstate < OSTREE_SYSROOT_LOAD_STATE_LOADED) { diff --git a/src/libostree/ostree-sysroot.h b/src/libostree/ostree-sysroot.h index 327c6d45..c1fb1482 100644 --- a/src/libostree/ostree-sysroot.h +++ b/src/libostree/ostree-sysroot.h @@ -271,11 +271,15 @@ gboolean ostree_sysroot_simple_write_deployment (OstreeSysroot *sysroot, const c _OSTREE_PUBLIC gboolean ostree_sysroot_deployment_can_soft_reboot (OstreeSysroot *self, OstreeDeployment *deployment); -_OSTREE_PUBLIC gboolean ostree_sysroot_deployment_prepare_next_root (OstreeSysroot *self, - OstreeDeployment *deployment, - gboolean allow_kernel_skew, - GCancellable *cancellable, - GError **error); +_OSTREE_PUBLIC gboolean ostree_sysroot_deployment_set_soft_reboot (OstreeSysroot *self, + OstreeDeployment *deployment, + gboolean allow_kernel_skew, + GCancellable *cancellable, + GError **error); + +_OSTREE_PUBLIC gboolean ostree_sysroot_clear_soft_reboot (OstreeSysroot *self, + GCancellable *cancellable, + GError **error); _OSTREE_PUBLIC gboolean ostree_sysroot_deployment_kexec_load (OstreeSysroot *self, OstreeDeployment *deployment, diff --git a/src/libotcore/otcore.h b/src/libotcore/otcore.h index 24b4d6cb..19348be7 100644 --- a/src/libotcore/otcore.h +++ b/src/libotcore/otcore.h @@ -140,6 +140,8 @@ gboolean otcore_mount_etc (GKeyFile *config, GVariantBuilder *metadata_builder, // from ostree-prepare-root. #define OTCORE_RUN_BOOTED "/run/ostree-booted" // Written by ostree-soft-reboot.c with metadata about /run/nextroot +// that is then processed by ostree-boot-complete.c and turned into +// the canonical /run/ostree-booted. #define OTCORE_RUN_NEXTROOT_BOOTED "/run/ostree/nextroot-booted" // This key will be present if composefs was successfully used. #define OTCORE_RUN_BOOTED_KEY_COMPOSEFS "composefs" diff --git a/src/ostree/ot-admin-builtin-prepare-soft-reboot.c b/src/ostree/ot-admin-builtin-prepare-soft-reboot.c index 3151f6e2..c8ccda7b 100644 --- a/src/ostree/ot-admin-builtin-prepare-soft-reboot.c +++ b/src/ostree/ot-admin-builtin-prepare-soft-reboot.c @@ -26,7 +26,14 @@ #include "ot-admin-functions.h" #include "otutil.h" -static GOptionEntry options[] = { { NULL } }; +static gboolean opt_reboot; +static gboolean opt_reset; + +static GOptionEntry options[] + = { { "reboot", 0, 0, G_OPTION_ARG_NONE, &opt_reboot, "Initiate a soft reboot on success", + NULL }, + { "reset", 0, 0, G_OPTION_ARG_NONE, &opt_reset, "Undo queued soft reboot state", NULL }, + { NULL } }; gboolean ot_admin_builtin_prepare_soft_reboot (int argc, char **argv, OstreeCommandInvocation *invocation, @@ -40,6 +47,9 @@ ot_admin_builtin_prepare_soft_reboot (int argc, char **argv, OstreeCommandInvoca cancellable, error)) return FALSE; + if (opt_reset) + return ostree_sysroot_clear_soft_reboot (sysroot, cancellable, error); + if (argc < 2) { ot_util_usage_error (context, "INDEX must be specified", error); @@ -68,9 +78,15 @@ ot_admin_builtin_prepare_soft_reboot (int argc, char **argv, OstreeCommandInvoca return FALSE; } - if (!ostree_sysroot_deployment_prepare_next_root (sysroot, target_deployment, FALSE, cancellable, - error)) + if (!ostree_sysroot_deployment_set_soft_reboot (sysroot, target_deployment, FALSE, cancellable, + error)) return FALSE; + if (opt_reboot) + { + execlp ("systemctl", "systemctl", "soft-reboot", NULL); + return glnx_throw_errno_prefix (error, "exec(systemctl soft-reboot)"); + } + return TRUE; } diff --git a/src/ostree/ot-builtin-admin.c b/src/ostree/ot-builtin-admin.c index a89afb2b..4df9e690 100644 --- a/src/ostree/ot-builtin-admin.c +++ b/src/ostree/ot-builtin-admin.c @@ -42,8 +42,12 @@ static OstreeCommand admin_subcommands[] = { "Change the finalization locking state of the staged deployment" }, { "boot-complete", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN, ot_admin_builtin_boot_complete, "Internal command to run at boot after an update was applied" }, +#ifdef HAVE_SOFT_REBOOT { "impl-prepare-soft-reboot", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN, ot_admin_builtin_impl_prepare_soft_reboot, "Internal command to prepare soft reboot" }, + { "prepare-soft-reboot", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_prepare_soft_reboot, + "Prepare deployment for soft-reboot" }, +#endif { "state-overlay", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN, ot_admin_builtin_state_overlay, "Internal command to assemble a state overlay" }, { "init-fs", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_init_fs, @@ -59,8 +63,6 @@ static OstreeCommand admin_subcommands[] = { "rollback strings" }, { "post-copy", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_post_copy, "Update the repo and deployments as needed after a copy" }, - { "prepare-soft-reboot", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_prepare_soft_reboot, - "Prepare deployment for soft-reboot" }, { "set-origin", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_set_origin, "Set Origin and create a new origin file" }, { "status", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_status, "List deployments" }, diff --git a/tests/kolainst/destructive/soft-reboot.sh b/tests/kolainst/destructive/soft-reboot.sh index 84365258..3ba99e2f 100755 --- a/tests/kolainst/destructive/soft-reboot.sh +++ b/tests/kolainst/destructive/soft-reboot.sh @@ -3,15 +3,30 @@ set -xeuo pipefail . ${KOLA_EXT_DATA}/libinsttest.sh -require_writable_sysroot prepare_tmpdir +echo "testing boot=${AUTOPKGTEST_REBOOT_MARK:-}" + +# Print this by default on each boot +ostree admin status + +# Verify /sysroot readonly by default on each boot +test '!' -w /sysroot +findmnt -J /sysroot > findmnt.json +assert_jq findmnt.json '.filesystems[0].options | contains("ro")' +# But mount it writable now so we can make test commits conveniently +require_writable_sysroot + +assert_soft_reboot_count() { + assert_streq $(systemctl show -P SoftRebootsCount) $1 +} + case "${AUTOPKGTEST_REBOOT_MARK:-}" in "") # xref https://github.com/coreos/coreos-assembler/pull/2814 systemctl mask --now zincati - assert_streq $(systemctl show -P SoftRebootsCount) 0 + assert_soft_reboot_count 0 assert_status_jq '.deployments[0].pending | not' '.deployments[0].["soft-reboot-target"] | not' # Create a synthetic commit for upgrade @@ -23,7 +38,7 @@ case "${AUTOPKGTEST_REBOOT_MARK:-}" in # Deploy the new commit normally first ostree admin deploy --stage soft-reboot-test - assert_status_jq '.deployments[0].pending' '.deployments[0].["soft-reboot-target"] | not' + assert_status_jq '.deployments[0].staged' '.deployments[0].["soft-reboot-target"] | not' # Test prepare-soft-reboot command echo "Testing prepare-soft-reboot..." @@ -31,20 +46,20 @@ case "${AUTOPKGTEST_REBOOT_MARK:-}" in # Test human readable format ostree admin status > status.txt - assert_file_has_content_literal status.txt '(pending) (soft-reboot)' + assert_file_has_content_literal status.txt '(staged) (soft-reboot)' # And via JSON assert_status_jq '.deployments[0].pending' '.deployments[0].["soft-reboot-target"]' - # Verify the internal state file test -f /run/ostree/nextroot-booted + mountpoint /run/nextroot /tmp/autopkgtest-soft-reboot "2" ;; "2") # After soft reboot, verify we're running the new deployment echo "Verifying post-soft-reboot state..." - assert_streq $(systemctl show -P SoftRebootsCount) 1 + assert_soft_reboot_count 1 expected_commit=$(ostree rev-parse soft-reboot-test) @@ -58,10 +73,74 @@ case "${AUTOPKGTEST_REBOOT_MARK:-}" in test -f /etc/new-file-for-soft-reboot test -f /usr/share/test-file-for-soft-reboot - # Verify that soft-reboot-pending file is cleaned up + # Verify that soft-reboot state files are gone test '!' -f /run/ostree/nextroot-booted echo "Soft reboot test completed successfully!" + + # Now soft reboot again into the rollback which is not staged, + # and also exercise the immediate --reboot flag. + touch /etc/current-contents + /tmp/autopkgtest-soft-reboot-prepare "3" + ostree admin prepare-soft-reboot --reboot 1 + ;; + "3") + assert_soft_reboot_count 2 + + # Only from the first updated target + test '!' -f /etc/new-file-for-soft-reboot + test '!' -f /usr/share/test-file-for-soft-reboot + # And this was in the *previous* current /etc + test '!' -f /etc/current-contents + + echo "ok verified prepare" + + assert_status_jq '.deployments[0].["soft-reboot-target"] | not' + + ostree admin prepare-soft-reboot 0 + assert_status_jq '.deployments[0].["soft-reboot-target"]' + ostree admin prepare-soft-reboot --reset + assert_status_jq '.deployments[0].["soft-reboot-target"] | not' + test '!' -f /run/ostree/nextroot-booted + # Test idempotence + ostree admin prepare-soft-reboot --reset + assert_status_jq '.deployments[0].["soft-reboot-target"] | not' + test '!' -f /run/ostree/nextroot-booted + + echo "ok soft reboot 3" + + # Now, test the intersection of staged deployments and soft rebooting + # Create another synthetic commit + cd /ostree/repo/tmp + ostree checkout -H ${host_commit} t + unshare -m /bin/sh -c 'mount -o remount,rw /sysroot && cd /ostree/repo/tmp/t && touch usr/share/test-staged-2-for-soft-reboot' + ostree commit --no-bindings --parent="${host_commit}" -b soft-reboot-test-staged-2 -I --consume t + newcommit=$(ostree rev-parse soft-reboot-test-staged-2) + ostree admin deploy --stage soft-reboot-test-staged-2 + + assert_status_jq '.deployments[0].staged' '.deployments[0].["soft-reboot-target"] | not' \ + '.deployments[1].booted | not' '.deployments[1].["soft-reboot-target"] | not' \ + '.deployments[2].booted' '.deployments[2].["soft-reboot-target"] | not' + + # Note here we're targeting the previous booted deployment, *not* the staged + ostree admin prepare-soft-reboot 1 + + # We set up the soft reboot, but cleared the staged + assert_status_jq '.deployments[0].booted | not' '.deployments[0].["soft-reboot-target"]' \ + '.deployments[1].booted' '.deployments[1].["soft-reboot-target"] | not' + + # And this one verifies we do a soft reboot by default as we've mounted /run/nextroot + /tmp/autopkgtest-soft-reboot-prepare "4" + systemctl reboot + ;; + "4") + assert_soft_reboot_count 3 + # Completion of soft reboot into non-staged + assert_status_jq '.deployments[0].booted' '.deployments[0].["soft-reboot-target"] | not' \ + '.deployments[1].booted | not' '.deployments[1].["soft-reboot-target"] | not' + echo "ok soft reboot to non-staged" + + echo "ok soft reboot all tests" ;; *) fatal "Unexpected AUTOPKGTEST_REBOOT_MARK=${AUTOPKGTEST_REBOOT_MARK}"